跳到主要内容

SpringBoot 使用 RestTemplate 发送网络请求

RESTful 风格请求

注意:Restful API 不能用于传输 HTML,一般都是传输 JSON

对资源的增删改查对应于 URL 的操作(POST、DELETE、PUT、GET)

都是同名,但是请求的方式不同

GET /user:获取一个用户 POST /user:创建一个用户 PUT /user:修改一个用户 DELETE /user:删除一个用户

或者 POST /authorization:登陆 DELETE /authorization:退出

就是一个不同于传统那种直接 ?id=1&name=Lisa 那样带参数的请求 而是直接把这个参数写入地址里,通过自定义的方法来读取出来

# 传统
http://127.0.0.1/project?id=1&name=Lisa
# 使用RestFul风格
http://127.0.0.1/project/1/Lisa

注意:这玩意不是一种标准或者协议,只是一种风格

// http://localhost:8080/test/1/4
@Controller
public class RestFulController {

@RequestMapping("/test/{p1}/{p2}")
public String test(@PathVariable("p1") int a, @PathVariable("p2") int b, Model model) {
model.addAttribute("msg", "结果为:" + (a + b));
return "test";
}
}

RestTemplate 是什么

转载自 如何使用RestTemplate访问restful服务

之前的 HTTP 开发是用 apache 的 HttpClient 开发,代码复杂,还得操心资源回收等

在项目中,当我们需要远程调用一个 HTTP 接口时,我们经常会用到 RestTemplate 这个类,RestTemplate 提供了多种便捷访问远程 Http 服务的方法,但是该工具类是同步的,所以会给阻塞(因此后来引入了 WebClient 这个新工具)

可以在在 Bean 上配置信息(有些 api 返回的数据返回的数据是经过 Gzip 压缩过的,所以需要使用一个库用来支持这个 Gzip 响应数据)

配置环境

<!-- 首先导入springboot 的 web 包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 使用 httpclient 添加支持 Gzip 的编码 -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>

配置这个 RestConfiguration

@Configuration
public class RestConfiguration {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate(
new HttpComponentsClientHttpRequestFactory()); // 使用HttpClient,支持GZIP
restTemplate.getMessageConverters().set(1,
new StringHttpMessageConverter(StandardCharsets.UTF_8)); // 支持中文编码
return restTemplate;
}

@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory(){
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(15000);
factory.setReadTimeout(5000);
return factory;
}
}

这个 ClientHttpRequestFactory 是在注入 RestTemplate 的 Bean的时候,可以通过 ClientHttpRequestFactory 指定 RestTemplate 发起 HTTP 请求的底层实现细节,具体看下面 “配置访问的信息” 那一节

然后就能直接通过 @Autowired 自动注入

@Service
public class demoService {

@Autowired
private RestTemplate restTemplate;

public String get(Integer id){
return restTemplate.getForObject("http://localhost:8080/user?userId=id", String.class);
}
}

简单使用示例

定义一个简单的 restful 接口

@RestController
public class TestController
{
@RequestMapping(value = "testPost", method = RequestMethod.POST)
public ResponseBean testPost(@RequestBody RequestBean requestBean)
{
ResponseBean responseBean = new ResponseBean();
responseBean.setRetCode("0000");
responseBean.setRetMsg("succ");

return responseBean;
}
}

使用 RestTemplate 访问该服务

//请求地址
String url = "http://localhost:8080/testPost";
//入参
RequestBean requestBean = new RequestBean();
requestBean.setTest1("1");
requestBean.setTest2("2");
requestBean.setTest3("3");

RestTemplate restTemplate = new RestTemplate();
ResponseBean responseBean = restTemplate.postForObject(url, requestBean, ResponseBean.class);

url, requestMap, ResponseBean.class 这三个参数分别代表 请求地址、请求参数、HTTP 响应转换被转换成的对象类型。

常用方法

参考资料 springboot 2.0 整合 RestTemplate 与使用教程

下面列举一些常用的方法

delete():              这个方法是在特定的 URL 上对资源执行 HTTP DELETE 操作
exchange(): 在 URL 上执行特定的 HTTP 方法,返回包含对象的 ResponseEntity,这个对象是从响应体中映射得到的
execute(): 在 URL 上执行特定的 HTTP 方法,返回一个从响应体映射得到的对象
getForEntity(): 发送一个 HTTP GET 请求,返回的 ResponseEntity 包含了响应体所映射成的对象
getForObject(): 发送一个 HTTP GET 请求,返回的请求体将映射为一个对象
postForEntity(): POST 数据到一个 URL,返回包含一个对象的 ResponseEntity,这个对象是从响应体中映射得到的
postForObject(): POST 数据到一个 URL,返回根据响应体匹配形成的对象
headForHeaders(): 发送 HTTP HEAD 请求,返回包含特定资源 URL 的 HTTP 头
optionsForAllow(): 发送 HTTP OPTIONS 请求,返回对特定 URL 的 Allow 头信息
postForLocation(): POST 数据到一个 URL,返回新创建资源的 URL
put(): PUT 资源到特定的 URL

getForEntity

get请求就和正常在浏览器url上发送请求一样

@GetMapping("getForEntity/{id}")
public User getById(@PathVariable(name = "id") String id) {
ResponseEntity<User> response = restTemplate.getForEntity("http://localhost/get/{id}", User.class, id);
User user = response.getBody();
return user;
}

getForObject

getForObject 和 getForEntity 用法几乎相同,只是它的返回值返回的是响应体,省去了我们再去 getBody()

@GetMapping("getForObject/{id}")
public User getById(@PathVariable(name = "id") String id) {
User user = restTemplate.getForObject("http://localhost/get/{id}", User.class, id);
return user;
}

postForEntity

@RequestMapping("saveUser")
public String save(User user) {
// postForEntity 参数:URL、请求参数、响应类型
ResponseEntity<String> response = restTemplate.postForEntity("http://localhost/save", user, String.class);
String body = response.getBody();
return body;
}

postForObject 用法与 getForObject 一样,这里略

exchange

在 URL 上执行特定的 HTTP 方法,返回包含对象的 ResponseEntity,这个对象是从响应体中映射得到的

@PostMapping("demo")
public void demo(Integer id, String name){

HttpHeaders headers = new HttpHeaders();//header参数
headers.add("authorization",Auth);
headers.setContentType(MediaType.APPLICATION_JSON);

JSONObject obj = new JSONObject();//放入body中的json参数
obj.put("userId", id);
obj.put("name", name);

HttpEntity<JSONObject> request = new HttpEntity<>(content,headers); //组装

ResponseEntity<String> response = template.exchange("http://localhost:8080/demo",HttpMethod.POST,request,String.class);
}

自定义转换器

参考资料 自定义HttpMessageConverter

HttpMessageConverter 是用来处理 request 和 response 里的数据的

调用 Restful 接口传递的数据内容是 Json 格式的字符串,返回的响应也是 Json 格式的字符串。然而 restTemplate.postForObject 方法的请求参数 RequestBean 和返回参数 ResponseBean 却都是 Java类。是 RestTemplate 通过 HttpMessageConverter 自动帮我们做了转换的操作。

默认情况下 RestTemplate 自动帮我们注册了一组 HttpMessageConverter 用来处理一些不同的 contentType 的请求。

如 StringHttpMessageConverter 来处理 text/plain; MappingJackson2HttpMessageConverter 来处理 application/json; MappingJackson2XmlHttpMessageConverter 来处理 application/xml

可以在 org.springframework.http.converter 包下找到所有 Spring 帮我们实现好的转换器。

public class SettingConverter extends AbstractHttpMessageConverter<SettingInfoRequest> {
/**
* 定义字符编码,防止乱码
*/
private static final Charset DEFAULT_CHARSET = Charsets.UTF_8;

/**
* 新建自定义的媒体类型
*/
public SettingConverter() {
super(new MediaType("application", "json", DEFAULT_CHARSET));
}

/**
* 表明只处理Settings这个类
*/
@Override
protected boolean supports(Class<?> aClass) {
return SettingInfoRequest.class.isAssignableFrom(aClass);
}

/**
* 重写readInternal方法,处理请求的数据
*/
@Override
protected SettingInfoRequest readInternal(Class<? extends SettingInfoRequest> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
final String temp = StreamUtils.copyToString(httpInputMessage.getBody(), DEFAULT_CHARSET);
if (temp.contains(TRAINING.name())) {
return JSONObject.parseObject(temp, new TypeReference<SettingInfoRequest<SettingInfo>>(){});
} else if (temp.contains(MODELDUMP.name())) {
return JSONObject.parseObject(temp, new TypeReference<SettingInfoRequest<DumpSettings>>(){});
}
return null;
}

/**
* 重写writeInternal,处理如何输出数据到response
*/
@Override
protected void writeInternal(SettingInfoRequest request, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
final String out = FastJsonUtils.toJSONString(request);
StreamUtils.copy(out, DEFAULT_CHARSET, httpOutputMessage.getBody());
}
}

配置自定义 Converter

@Configuration
public class AlphaWebConfig extends WebMvcConfigurerAdapter {

/**
* 添加自定义的httpMessageConverter
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(messageConverter());
}

@Bean
public SettingConverter messageConverter(){
return new SettingConverter();
}

}

手动指定转换器

配置好了 HttpMessageConverter 后怎么把它注册到我们的 RestTemplate 中呢

RestTemplate restTemplate = new RestTemplate();
//获取RestTemplate默认配置好的所有转换器
List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters();
//默认的MappingJackson2HttpMessageConverter在第7个 先把它移除掉
messageConverters.remove(6);
//添加上GSON的转换器
messageConverters.add(6, new GsonHttpMessageConverter());

配置访问的信息

要创建一个 RestTemplate 的实例,可以像上述例子中简单地调用默认的无参数构造函数。这将使用 java.net 包中的标准 Java 类作为底层实现来创建 HTTP 请求。

但很多时候我们需要像传统的 HttpClient 那样设置 HTTP 请求的一些属性。RestTemplate 使用了一种很偷懒的方式实现了这个需求,那就是直接使用一个 HttpClient 作为底层实现

//生成一个设置了连接超时时间、请求超时时间、异常最大重试次数的httpClient
RequestConfig config = RequestConfig
.custom()
.setConnectionRequestTimeout(10000)
.setConnectTimeout(10000)
.setSocketTimeout(30000).build();

HttpClientBuilder builder = HttpClientBuilder
.create()
.setDefaultRequestConfig(config)
.setRetryHandler(new DefaultHttpRequestRetryHandler(5, false));

HttpClient httpClient = builder.build();

//使用httpClient创建一个ClientHttpRequestFactory的实现
ClientHttpRequestFactory requestFactory =
new HttpComponentsClientHttpRequestFactory(httpClient);

//ClientHttpRequestFactory作为参数构造一个使用作为底层的RestTemplate
RestTemplate restTemplate = new RestTemplate(requestFactory);

设置拦截器

有时候我们需要对请求做一些通用的拦截设置,这就可以使用拦截器进行处理。拦截器需要我们实现 org.springframework.http.client.ClientHttpRequestInterceptor 接口自己写。

举个简单的例子,写一个在 header 中根据请求内容和地址添加令牌的拦截器。

public class TokenInterceptor implements ClientHttpRequestInterceptor
{
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException
{
//请求地址
String checkTokenUrl = request.getURI().getPath();
//token有效时间
int ttTime = (int) (System.currentTimeMillis() / 1000 + 1800);
//请求方法名 POST、GET等
String methodName = request.getMethod().name();
//请求内容
String requestBody = new String(body);
//生成令牌 此处调用一个自己写的方法,有兴趣的朋友可以自行google如何使用 ak/sk 生成token,此方法跟本教程无关,就不贴出来了
String token = TokenHelper.generateToken(checkTokenUrl, ttTime, methodName, requestBody);
//将令牌放入请求header中
request.getHeaders().add("X-Auth-Token",token);

return execution.execute(request, body);
}
}

创建 RestTemplate 实例的时候可以这样向其中添加拦截器

RestTemplate restTemplate = new RestTemplate();
//向restTemplate中添加自定义的拦截器
restTemplate.getInterceptors().add(new TokenInterceptor());

有时候,我们需要在请求中的 Head 中添加值或者将某些值通过 cookie 传给服务端

 UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl("127.0.0.1:8080").
path("/test").build(true);
URI uri = uriComponents.toUri();

RequestEntity<JSONObject> requestEntity = RequestEntity.post(uri).
// 添加 cookie(这边有个问题,假如我们要设置 cookie 的生命周期,作用域等参数我们要怎么操作)
header(HttpHeaders.COOKIE,"key1=value1").
// 添加 header
header(("MyRequestHeader", "MyValue")
accept(MediaType.APPLICATION_JSON).
contentType(MediaType.APPLICATION_JSON).
body(requestParam);
ResponseEntity<JSONObject> responseEntity = restTemplate.exchange(requestEntity,JSONObject.class);
// 响应结果
JSONObject responseEntityBody = responseEntity.getBody();

为什么要使用 MultiValueMap

HttpEntity httpEntity = new HttpEntity<>(body, headers);

restTemplate.exchange(getUrl(), httpMethod, httpEntity, responseType);

这里的 HttpEntity 可以发现后面那个 headers 是 MultiValueMap 类型的,可是请求头不应该是 Key-Value 形式的吗?为什么会有这种 Key-Value、Value、Value 的 Map

因为一些请求头是这种一对多的形式,例如 Accept,它标识了浏览器可以处理的内容列表,还有 cookie 也是属于 header 的一部分